// @ts-check
// rollup.config.js - Vue3 项目的 Rollup 构建配置文件
// 该文件定义了所有 Vue3 子包的构建配置，支持多种输出格式和生产/开发环境构建

// 导入 Node.js 内置模块
import assert from 'node:assert/strict' // 严格断言，用于验证配置参数
import { createRequire } from 'node:module' // 在 ESM 中创建 require 函数
import { fileURLToPath } from 'node:url' // 将 URL 转换为文件路径
import fs from 'node:fs' // 文件系统操作
import path from 'node:path' // 路径处理工具

// 导入 Rollup 插件
import replace from '@rollup/plugin-replace' // 字符串替换插件
import json from '@rollup/plugin-json' // 处理 JSON 文件的插件
import pico from 'picocolors' // 控制台彩色输出
import commonJS from '@rollup/plugin-commonjs' // 将 CommonJS 模块转换为 ES 模块
import polyfillNode from 'rollup-plugin-polyfill-node' // Node.js 环境的 polyfill
import { nodeResolve } from '@rollup/plugin-node-resolve' // 解析 node_modules 中的依赖
import esbuild from 'rollup-plugin-esbuild' // 使用 esbuild 处理 TypeScript
import alias from '@rollup/plugin-alias' // 路径别名插件

// 导入项目自定义模块
import { entries } from './scripts/aliases.js' // 路径别名配置
import { inlineEnums } from './scripts/inline-enums.js' // 内联枚举处理
import { minify as minifySwc } from '@swc/core' // SWC 代码压缩工具

// TypeScript 类型定义
/**
 * 工具类型：标记某些属性为必需
 * @template T
 * @template {keyof T} K
 * @typedef { Omit<T, K> & Required<Pick<T, K>> } MarkRequired
 */

/**
 * 包构建格式枚举类型
 * - cjs: CommonJS 格式，用于 Node.js 环境
 * - esm-bundler: ES 模块格式，用于构建工具（如 webpack、vite）
 * - global: 全局变量格式，直接在浏览器中使用
 * - global-runtime: 仅运行时的全局构建（不包含编译器）
 * - esm-browser: 浏览器原生 ES 模块格式
 * - esm-bundler-runtime: 仅运行时的 ES 模块构建（用于构建工具）
 * - esm-browser-runtime: 仅运行时的浏览器 ES 模块构建
 * @typedef {'cjs' | 'esm-bundler' | 'global' | 'global-runtime' | 'esm-browser' | 'esm-bundler-runtime' | 'esm-browser-runtime'} PackageFormat
 */

/**
 * Rollup 输出配置类型
 * @typedef {MarkRequired<import('rollup').OutputOptions, 'file' | 'format'>} OutputOptions
 */

// 验证环境变量 TARGET 是否设置
// TARGET 指定要构建的包名称，通过 --environment TARGET=xxx 传递
if (!process.env.TARGET) {
  throw new Error('必须通过 --environment 标志指定 TARGET 包名')
}

// 创建 require 函数和目录路径（ES 模块兼容性处理）
const require = createRequire(import.meta.url) // 在 ES 模块中创建 require 函数
const __dirname = fileURLToPath(new URL('.', import.meta.url)) // 获取当前文件所在目录

// 读取版本信息
const masterVersion = require('./package.json').version // 主版本号，用于所有包的版本标识
const consolidatePkg = require('@vue/consolidate/package.json') // consolidate 包信息，用于编译器配置

// 确定包所在的基础目录（区分公共包和私有包）
const privatePackages = fs.readdirSync('packages-private') // 读取私有包目录列表
const pkgBase = privatePackages.includes(process.env.TARGET)
  ? `packages-private` // 如果目标包是私有包，使用私有包目录
  : `packages` // 否则使用公共包目录

// 构建路径
const packagesDir = path.resolve(__dirname, pkgBase) // 包的基础目录绝对路径
const packageDir = path.resolve(packagesDir, process.env.TARGET) // 目标包的绝对路径

// 辅助函数：解析相对于包目录的路径
const resolve = (/** @type {string} */ p) => path.resolve(packageDir, p)

// 读取包配置
const pkg = require(resolve(`package.json`)) // 目标包的 package.json 配置
const packageOptions = pkg.buildOptions || {} // 包的构建选项，如果未定义则使用空对象
const name = packageOptions.filename || path.basename(packageDir) // 构建文件名，优先使用配置的 filename

// 构建产物的版权信息横幅
const banner = `/**
* ${pkg.name} v${masterVersion}
* (c) 2018-present Yuxi (Evan) You and Vue contributors
* @license MIT
**/`

// 初始化枚举内联处理插件
// enumPlugin: Rollup 插件实例，用于内联枚举
// enumDefines: 枚举常量定义，用于代码替换
const [enumPlugin, enumDefines] = inlineEnums()

// 定义各种构建格式的输出配置
/** @type {Record<PackageFormat, OutputOptions>} */
const outputConfigs = {
  // ES 模块格式，用于构建工具（webpack、vite 等）
  'esm-bundler': {
    file: resolve(`dist/${name}.esm-bundler.js`),
    format: 'es', // ES 模块格式
  },
  // ES 模块格式，用于现代浏览器（支持原生 ES 模块）
  'esm-browser': {
    file: resolve(`dist/${name}.esm-browser.js`),
    format: 'es', // ES 模块格式
  },
  // CommonJS 格式，用于 Node.js 环境
  cjs: {
    file: resolve(`dist/${name}.cjs.js`),
    format: 'cjs', // CommonJS 格式
  },
  // 全局变量格式，通过 script 标签直接在浏览器中使用
  global: {
    file: resolve(`dist/${name}.global.js`),
    format: 'iife', // 立即调用函数表达式格式
  },
  // 以下是仅运行时版本的构建（不包含模板编译器），主要用于 "vue" 主包
  // 运行时版本的 ES 模块格式，用于构建工具
  'esm-bundler-runtime': {
    file: resolve(`dist/${name}.runtime.esm-bundler.js`),
    format: 'es',
  },
  // 运行时版本的 ES 模块格式，用于现代浏览器
  'esm-browser-runtime': {
    file: resolve(`dist/${name}.runtime.esm-browser.js`),
    format: 'es',
  },
  // 运行时版本的全局变量格式，用于浏览器
  'global-runtime': {
    file: resolve(`dist/${name}.runtime.global.js`),
    format: 'iife',
  },
}

// 构建格式配置
/** @type {ReadonlyArray<PackageFormat>} */
const defaultFormats = ['esm-bundler', 'cjs'] // 默认构建格式

/** @type {ReadonlyArray<PackageFormat>} */
// 通过环境变量指定的构建格式（FORMATS=esm-bundler,cjs）
const inlineFormats = /** @type {any} */ (
  process.env.FORMATS && process.env.FORMATS.split(',')
)

/** @type {ReadonlyArray<PackageFormat>} */
// 最终使用的构建格式：优先级为 环境变量 > 包配置 > 默认格式
const packageFormats = inlineFormats || packageOptions.formats || defaultFormats

// 生成包的构建配置数组
// 如果设置了 PROD_ONLY 环境变量，则跳过开发版本的构建
const packageConfigs = process.env.PROD_ONLY
  ? [] // 仅生产环境构建时，先返回空数组，后续会添加生产版本配置
  : packageFormats.map(format => createConfig(format, outputConfigs[format])) // 为每种格式创建构建配置

// 生产环境构建配置
if (process.env.NODE_ENV === 'production') {
  packageFormats.forEach(format => {
    // 如果包配置明确禁用生产构建，则跳过
    if (packageOptions.prod === false) {
      return
    }

    // 为 CommonJS 格式创建生产版本（.prod.js）
    if (format === 'cjs') {
      packageConfigs.push(createProductionConfig(format))
    }

    // 为全局构建和浏览器 ES 模块构建创建压缩版本
    if (/^(global|esm-browser)(-runtime)?/.test(format)) {
      packageConfigs.push(createMinifiedConfig(format))
    }
  })
}

// 导出所有构建配置
export default packageConfigs

/**
 * 创建 Rollup 构建配置
 * 这是核心函数，根据指定的构建格式生成完整的 Rollup 配置对象
 *
 * @param {PackageFormat} format 构建格式（cjs、esm-bundler、global 等）
 * @param {OutputOptions} output 输出配置（文件路径、格式等）
 * @param {ReadonlyArray<import('rollup').Plugin>} plugins 额外的插件数组
 * @returns {import('rollup').RollupOptions} 完整的 Rollup 配置对象
 */
function createConfig(format, output, plugins = []) {
  // 验证输出配置是否有效
  if (!output) {
    console.log(pico.yellow(`无效的构建格式: "${format}"`)) // 使用黄色输出错误信息
    process.exit(1) // 退出进程
  }

  // 构建类型判断标志
  const isProductionBuild =
    process.env.__DEV__ === 'false' || /\.prod\.js$/.test(output.file) // 是否为生产构建
  const isBundlerESMBuild = /esm-bundler/.test(format) // 是否为构建工具 ES 模块格式
  const isBrowserESMBuild = /esm-browser/.test(format) // 是否为浏览器 ES 模块格式
  const isServerRenderer = name === 'server-renderer' // 是否为服务端渲染包
  const isCJSBuild = format === 'cjs' // 是否为 CommonJS 构建
  const isGlobalBuild = /global/.test(format) // 是否为全局变量构建
  const isCompatPackage =
    pkg.name === '@vue/compat' || pkg.name === '@vue/compat-canary' // 是否为兼容性包
  const isCompatBuild = !!packageOptions.compat // 是否启用兼容性构建
  const isBrowserBuild =
    (isGlobalBuild || isBrowserESMBuild || isBundlerESMBuild) &&
    !packageOptions.enableNonBrowserBranches // 是否为浏览器环境构建

  // 配置输出选项
  output.banner = banner // 添加版权信息横幅

  // 导出类型配置：兼容性包使用 auto，其他使用 named 导出
  output.exports = isCompatPackage ? 'auto' : 'named'

  // CommonJS 构建启用 ES 模块兼容性
  if (isCJSBuild) {
    output.esModule = true // 生成 __esModule 标记
  }

  output.sourcemap = !!process.env.SOURCE_MAP // 根据环境变量决定是否生成源码映射
  output.externalLiveBindings = false // 禁用外部模块的实时绑定
  output.reexportProtoFromExternal = false // 禁用从外部模块重新导出原型

  // 全局构建需要指定全局变量名
  if (isGlobalBuild) {
    output.name = packageOptions.name // 使用包配置中的名称作为全局变量名
  }

  // 确定入口文件
  // runtime 结尾的格式使用 runtime.ts，否则使用 index.ts
  let entryFile = /runtime$/.test(format) ? `src/runtime.ts` : `src/index.ts`

  // 兼容性包的特殊处理
  // 兼容性包需要同时支持默认导出和命名导出，这会导致 Rollup 对非 ES 模块目标报警
  // 因此对 ES 模块和非 ES 模块构建使用不同的入口文件
  if (isCompatPackage && (isBrowserESMBuild || isBundlerESMBuild)) {
    entryFile = /runtime$/.test(format)
      ? `src/esm-runtime.ts` // ES 模块版本的运行时入口
      : `src/esm-index.ts` // ES 模块版本的主入口
  }

  /**
   * 解析编译时常量定义
   * 返回用于 esbuild define 选项的常量替换对象
   * 这些常量在编译时会被对应的值替换，用于条件编译和优化
   */
  function resolveDefine() {
    /** @type {Record<string, string>} */
    const replacements = {
      __COMMIT__: `"${process.env.COMMIT}"`, // Git 提交哈希值
      __VERSION__: `"${masterVersion}"`, // Vue 版本号
      __TEST__: `false`, // 测试环境标志（仅用于 Vue 内部测试）

      // 构建目标环境标志
      __BROWSER__: String(isBrowserBuild), // 是否为浏览器环境构建
      __GLOBAL__: String(isGlobalBuild), // 是否为全局变量构建
      __ESM_BUNDLER__: String(isBundlerESMBuild), // 是否为构建工具 ESM 构建
      __ESM_BROWSER__: String(isBrowserESMBuild), // 是否为浏览器 ESM 构建
      __CJS__: String(isCJSBuild), // 是否为 CommonJS 构建
      __SSR__: String(!isGlobalBuild), // 是否需要 SSR 特定的代码分支

      __COMPAT__: String(isCompatBuild), // 是否为 2.x 兼容性构建

      // Vue 3 特性标志
      __FEATURE_SUSPENSE__: `true`, // Suspense 组件支持
      __FEATURE_OPTIONS_API__: isBundlerESMBuild
        ? `__VUE_OPTIONS_API__` // 构建工具中由用户控制
        : `true`, // 默认启用 Options API
      __FEATURE_PROD_DEVTOOLS__: isBundlerESMBuild
        ? `__VUE_PROD_DEVTOOLS__` // 生产环境开发者工具支持
        : `false`, // 默认禁用
      __FEATURE_PROD_HYDRATION_MISMATCH_DETAILS__: isBundlerESMBuild
        ? `__VUE_PROD_HYDRATION_MISMATCH_DETAILS__` // 水合不匹配详细信息
        : `false`,
    }

    // 对于非构建工具构建，硬编码开发/生产环境标志
    if (!isBundlerESMBuild) {
      replacements.__DEV__ = String(!isProductionBuild) // 开发环境标志
    }

    // 允许通过环境变量覆盖常量定义
    // 例如：__RUNTIME_COMPILE__=true pnpm build runtime-core
    Object.keys(replacements).forEach(key => {
      if (key in process.env) {
        const value = process.env[key]
        assert(typeof value === 'string')
        replacements[key] = value
      }
    })
    return replacements
  }

  /**
   * 解析字符串替换配置
   * esbuild 的 define 选项比较严格，只允许字面量 JSON 或标识符
   * 所以在某些情况下仍需要使用 replace 插件
   */
  function resolveReplace() {
    const replacements = { ...enumDefines } // 复制枚举常量定义

    // 生产环境浏览器构建：为错误处理函数添加 PURE 注释，便于 Tree Shaking
    if (isProductionBuild && isBrowserBuild) {
      Object.assign(replacements, {
        'context.onError(': `/*@__PURE__*/ context.onError(`,
        'emitError(': `/*@__PURE__*/ emitError(`,
        'createCompilerError(': `/*@__PURE__*/ createCompilerError(`,
        'createDOMCompilerError(': `/*@__PURE__*/ createDOMCompilerError(`,
      })
    }

    // 构建工具 ESM 构建：保留 __DEV__ 交由构建工具处理
    if (isBundlerESMBuild) {
      Object.assign(replacements, {
        __DEV__: `!!(process.env.NODE_ENV !== 'production')`, // 动态开发环境检测
      })
    }

    // 浏览器 ES 模块构建：替换 Node.js 环境变量为空对象
    if (isBrowserESMBuild) {
      Object.assign(replacements, {
        'process.env': '({})', // 空环境变量对象
        'process.platform': '""', // 空平台字符串
        'process.stdout': 'null', // 空标准输出
      })
    }

    // 如果有替换项，返回 replace 插件配置
    if (Object.keys(replacements).length) {
      return [replace({ values: replacements, preventAssignment: true })]
    } else {
      return []
    }
  }

  /**
   * 解析外部依赖配置
   * 确定哪些依赖应该作为外部依赖处理，不打包到最终的构建产物中
   */
  function resolveExternal() {
    // 这些依赖只用于 Tree Shaking，列在这里以抑制警告
    const treeShakenDeps = [
      'source-map-js', // 源码映射支持
      '@babel/parser', // Babel 解析器
      'estree-walker', // AST 遍历工具
      'entities/lib/decode.js', // HTML 实体解码
    ]

    // 浏览器构建或兼容性包：将非浏览器依赖作为外部依赖处理
    if (isGlobalBuild || isBrowserESMBuild || isCompatPackage) {
      if (!packageOptions.enableNonBrowserBranches) {
        // 正常浏览器构建：非浏览器依赖会被 Tree Shaking 移除
        // 这里列出它们只是为了抑制警告
        return treeShakenDeps
      }
    } else {
      // Node.js / ESM 构建工具构建：将所有直接依赖作为外部依赖
      return [
        ...Object.keys(pkg.dependencies || {}), // 生产依赖
        ...Object.keys(pkg.peerDependencies || {}), // 同等依赖
        ...['path', 'url', 'stream'], // Node.js 内置模块（用于 compiler-sfc 和 server-renderer）
        ...treeShakenDeps, // Tree Shaking 依赖
      ]
    }
  }

  /**
   * 解析 Node.js 相关插件
   * 根据构建类型决定是否需要 CommonJS、polyfill 和节点解析插件
   */
  function resolveNodePlugins() {
    // compiler-sfc 中打包了 consolidate.js 的分支版本
    // 它动态 require 大量模板引擎，这些应该被忽略
    /** @type {ReadonlyArray<string>} */
    let cjsIgnores = []
    if (
      pkg.name === '@vue/compiler-sfc' ||
      pkg.name === '@vue/compiler-sfc-canary'
    ) {
      cjsIgnores = [
        ...Object.keys(consolidatePkg.devDependencies), // consolidate 的开发依赖
        'vm', // Node.js vm 模块
        'crypto', // Node.js crypto 模块
        'react-dom/server', // React 服务端渲染
        'teacup/lib/express', // Teacup Express 支持
        'arc-templates/dist/es5', // Arc 模板
        'then-pug', // Pug 异步支持
        'then-jade', // Jade 异步支持
      ]
    }

    // 决定是否需要 Node.js 插件
    const nodePlugins =
      (format === 'cjs' && Object.keys(pkg.devDependencies || {}).length) || // CJS 构建且有开发依赖
      packageOptions.enableNonBrowserBranches // 或者启用了非浏览器分支
        ? [
            // CommonJS 插件：将 CommonJS 模块转换为 ES 模块
            commonJS({
              sourceMap: false, // 禁用源码映射
              ignore: cjsIgnores, // 忽略列表
            }),
            // polyfill 插件：为非 CJS 构建提供 Node.js polyfill
            ...(format === 'cjs' ? [] : [polyfillNode()]),
            // 节点解析插件：解析 node_modules 中的模块
            nodeResolve(),
          ]
        : [] // 不需要 Node.js 插件

    return nodePlugins
  }

  // 返回完整的 Rollup 配置对象
  return {
    input: resolve(entryFile), // 入口文件路径
    external: resolveExternal(), // 外部依赖配置
    plugins: [
      // JSON 插件：处理 JSON 文件导入
      json({
        namedExports: false, // 禁用命名导出，使用默认导出
      }),
      // 别名插件：路径别名解析
      alias({
        entries, // 使用项目配置的别名映射
      }),
      enumPlugin, // 枚举内联插件
      ...resolveReplace(), // 字符串替换插件
      // esbuild 插件：TypeScript 编译和代码转换
      esbuild({
        tsconfig: path.resolve(__dirname, 'tsconfig.json'), // TypeScript 配置文件
        sourceMap: output.sourcemap, // 源码映射配置
        minify: false, // 禁用内置压缩（由后续插件处理）
        target: isServerRenderer || isCJSBuild ? 'es2019' : 'es2016', // 目标 ES 版本
        define: resolveDefine(), // 编译时常量定义
      }),
      ...resolveNodePlugins(), // Node.js 相关插件
      ...plugins, // 额外的自定义插件
    ],
    output, // 输出配置
    // 警告处理：忽略循环依赖警告
    onwarn: (msg, warn) => {
      if (msg.code !== 'CIRCULAR_DEPENDENCY') {
        warn(msg) // 输出其他警告
      }
    },
    // Tree Shaking 配置
    treeshake: {
      moduleSideEffects: false, // 假设模块无副作用，允许更激进的 Tree Shaking
    },
  }
}

/**
 * 创建生产环境构建配置
 * 为 CommonJS 格式创建 .prod.js 后缀的生产版本
 * @param {PackageFormat} format 构建格式
 */
function createProductionConfig(/** @type {PackageFormat} */ format) {
  return createConfig(format, {
    file: resolve(`dist/${name}.${format}.prod.js`), // 生产版本文件名
    format: outputConfigs[format].format, // 使用原始格式配置
  })
}

/**
 * 创建压缩构建配置
 * 为全局构建和浏览器 ES 模块构建创建压缩版本
 * @param {PackageFormat} format 构建格式
 */
function createMinifiedConfig(/** @type {PackageFormat} */ format) {
  return createConfig(
    format,
    {
      // 将文件名中的 .js 替换为 .prod.js
      file: outputConfigs[format].file.replace(/\.js$/, '.prod.js'),
      format: outputConfigs[format].format,
    },
    [
      // 添加 SWC 压缩插件
      {
        name: 'swc-minify', // 插件名称

        // 在代码块渲染阶段执行压缩
        async renderChunk(contents, _, { format }) {
          const { code } = await minifySwc(contents, {
            module: format === 'es', // ES 模块格式优化
            format: {
              comments: false, // 移除注释
            },
            compress: {
              ecma: 2016, // 目标 ECMAScript 版本
              pure_getters: true, // 假设 getter 无副作用
            },
            safari10: true, // Safari 10 兼容性
            mangle: true, // 变量名混淆
          })

          // 返回压缩后的代码，带上版权信息横幅
          return { code: banner + code, map: null }
        },
      },
    ],
  )
}
